1. MySQL 特训指南(极简源码速成版)
本指南专为快速吃透 SaaS 订单流转条件更新(并发写冲突)、唯一约束幂等表设计 以及 秒杀减库存防超卖 等高频核心场景中的 MySQL 考点而设计。杜绝大段铺垫,采用 Why - What - How - Deep 四步直达底层源码与物理页分配,帮助您在面试中反客为主,化被动为主动。
🚀 核心概念极简拆解
- 回表 (Lookup)
- Why:辅助索引只保存索引键值和主键值,无法存放整行数据的全部属性列(以节省非叶子节点内存)。
- What/How:先在辅助索引树中定位到主键,再拿着主键去聚簇索引树二次检索获取整行记录的过程。这会带来高昂的随机磁盘 I/O。
- 覆盖索引 (Covering Index)
- Why:回表会产生多余的随机磁盘 I/O。如果能直接在索引中拿到需要的所有字段,则可实现零回表。
- What/How:一个查询中需要的全部数据列,直接在一棵辅助索引联合索引树中就能完全拿到,EXPLAIN 的 Extra 字段会显示为
Using index。
- 写前日志 (Write-Ahead Logging, WAL)
- Why:磁盘物理数据页(ibd 文件)的随机写入速度极慢。如果每次修改都直接刷盘,数据库写吞吐当场瘫痪。
- What/Deep:对数据的任何改动首先写入内存,并在内存中追加写入顺序落盘极为高效的重做日志(Redo Log),之后再由后台线程异步把脏页同步回磁盘,以内存速度换取持久性保障。
- 意向锁 (Intention Lock)
- Why:当事务尝试给全表加表锁时,如果不引入意向锁,就必须逐行扫描整张表上百万条记录看有没有加行级排他锁,复杂度高达 $O(N)$。
- What/Deep:一种表级标志锁(IS/IX 锁)。加行 S 锁前自动加表 IS 锁,加行 X 锁前自动加表 IX 锁。当另一个事务准备加表锁时,直接检查表级 IX 标志,瞬间将锁判定复杂度从 $O(N)$ 降低为 $O(1)$。
- 索引下推 (Index Condition Pushdown, ICP)
- Why:在没有 ICP 之前,联合索引如果因为中间范围查询失效,后续过滤条件必须将全部候选数据回表后,再拿到 MySQL Server 层逐个执行条件判断,耗尽网络与回表 I/O。
- What/How:MySQL 5.6 引入。在联合索引失效的情况下,存储引擎层并不急于回表,而是直接在索引中执行后续条件的先期过滤,筛掉不符合要求的行,极大缩减回表行数。
🚀 核心存储与交互骨架
在面试中谈及 InnoDB,您可以用下图快速展示其在物理磁盘与内存缓冲池(Buffer Pool)之间的 WAL 数据交互流转过程:
mermaid
graph TD
subgraph InnoDB 内存结构 (Memory Space)
BufferPool[缓冲池 Buffer Pool <br> 16KB 数据页]
ChangeBuffer[Change Buffer <br> 辅助索引写缓冲]
RedoBuffer[Redo Log Buffer]
end
subgraph 磁盘存储 (Disk Space)
ibdFile[表数据物理文件 *.ibd]
redoLog[重做日志 ib_logfile]
binlog[归档日志 mysql-bin]
end
BufferPool <-->|异步刷脏页 / 预读| ibdFile
ChangeBuffer -->|Merge 刷盘| ibdFile
RedoBuffer -->|WAL 高速顺序写| redoLog🎯 第一优先级核心考点详解
一、 MVCC 多版本并发控制与事务隔离 (Why-What-How-Deep)
- Why(为什么高并发读写不能加悲观锁?)
- 痛点:在高并发场景下,读写冲突极其激烈。如果读写互相等待阻塞排队,系统的读写吞吐会瞬间跌入谷底。
- 解决:InnoDB 采用 MVCC 机制实现了**“读写不冲突”**。即普通 SELECT(快照读)完全无需加锁,即可安全读取到事务历史快照数据,极大解放了读吞吐。
- What(MVCC 的三大底层基石)
- 隐藏列字段:
DB_TRX_ID:最近修改(插入/更新)该行记录的事务 ID。DB_ROLL_PTR:回滚指针,指向写入undo log的上一个旧版本,将所有历史版本串联成一个版本链。
- Undo Log (回滚日志) 版本链:
- 修改某行记录时,原旧数据被拷贝进 undo log,新记录的回滚指针指向该旧数据,形成版本链。
- ReadView (一致性视图):
- 进行普通 SELECT 时生成的活跃事务内存结构。包含:
m_ids:生成 ReadView 时系统里活跃且未提交的事务 ID 列表。min_trx_id:m_ids里的最小值(最老的未提交事务 ID)。max_trx_id:系统预分配给下一个事务的 ID(当前最大事务 ID + 1)。creator_trx_id:创建这个 ReadView 的当前事务 ID。
- 进行普通 SELECT 时生成的活跃事务内存结构。包含:
- 隐藏列字段:
- How(可见性比较规则与隔离级别区别)
- 可见性比对算法:当事务尝试读取版本链中某条记录(其更新事务为
trx_id)时,遵循以下规则:trx_id == creator_trx_id:自己修改的,可见。trx_id < min_trx_id:生成 ReadView 前该事务已提交,可见。trx_id >= max_trx_id:生成 ReadView 后才开启的事务修改,不可见。min_trx_id <= trx_id < max_trx_id:如果在m_ids中,说明尚未提交,不可见(去版本链找上一版);不在m_ids中,说明已提交,可见。
- 可见性比对算法:当事务尝试读取版本链中某条记录(其更新事务为
- Deep(深入源码:RC 与 RR 隔离级别的 ReadView 生成时机分水岭)
- 读已提交 (RC) 隔离级别:
- 源码机制:在事务生命周期内,每一次普通 SELECT 都会重新生成一个新的 ReadView。
- 后果:因此,只要有其他并发事务提交,其事务 ID 就会从新 ReadView 的
m_ids活跃列表中剔除,导致两次 SELECT 看到的数据不一致,即发生了“不可重复读”。
- 可重复读 (RR) 隔离级别:
- 源码机制:在事务生命周期内,只有在第一次普通 SELECT 时才会生成一个 ReadView,后续所有的快照读均无条件复用这第一个 ReadView。
- 后果:这彻底锁死了可见性判定边界,无论并发事务如何提交,由于复用旧 ReadView,其
m_ids依然保持不变,从而天生在快照读层面解决了不可重复读与幻读问题。
- 读已提交 (RC) 隔离级别:
二、 B+ 树索引底层设计与大厂优化 (Why-What-How-Deep)
- Why(为什么 MySQL 选用 B+ 树而不是 B 树或二叉树?)
- 痛点:二叉树/红黑树树高极高,高频的磁盘 I/O 随机读写会导致严重的读写开销。B 树在非叶子节点中同时存储了 Key 和 Data(整行数据),这导致每个 16KB 的节点页面能容纳的索引项极其有限。
- 解决:B+ 树非叶子节点只存 Key,使单节点扇出度最大化。
- What(B+ 树的三大物理优势)
- 高度极矮:非叶子节点极度紧凑,一个 16KB 页面能存储上千个索引键,树的高度被极度压低在 3~4 层,千万级检索也只需 3~4 次磁盘 I/O。
- 双向链表相连:所有真实数据均保存在叶子节点中,且叶子节点间通过双向链表相连。在执行
BETWEEN范围检索时,只需一次二分定位起点,随后顺着双向链表横向拉取数据即可,免去了多级树节点的回溯遍历。
- How(大厂索引失效四大黄金坑)
- 隐式类型转换(最易发生的Bug):若字段
phone是varchar类型,写成WHERE phone = 17731930675,会导致 MySQL 自动调用CAST函数把索引列强转为数值型,彻底摧毁索引。 - 对索引列执行函数/表达式:
WHERE YEAR(create_time) = 2026(索引失效,需改写为WHERE create_time BETWEEN ...)。 - 模糊查询
%放在最前:WHERE name LIKE '%刚'(索引失效,必须是前缀匹配如'袁%')。 - 联合索引违背最左前缀:联合索引
(a, b, c),若写为WHERE b = 2 AND c = 3,没有最左前导列a,联合索引完全不生效。
- 隐式类型转换(最易发生的Bug):若字段
- Deep(深入物理存储:主键自增对页分裂 Page Split 与物理碎片的底层影响)
- B+ 树物理存储本质:InnoDB 的数据是存放在默认大小为 16KB 的**“页(Page)”**中的。这些页在物理磁盘上通过双向链表连结,而在页内部,记录行按主键值递增顺序以单向链表有序排列。
- 主键自增(顺序写入)的物理过程:
- 当主键为自增(顺序递增)时,新插入的行记录只需顺理成章地被追加追加在当前物理页的末尾。当一个页写满(超过 16KB),InnoDB 会顺畅地申请并分配一个全新的物理页继续追加写入。整个过程没有任何多余的物理移动,写入性能拉满。
- 主键无序(如 UUID 插入)引发的页分裂(Page Split)灾难:
- 如果主键使用 UUID,由于其无序性,新插入的记录会被强行要求塞入到已经写满的、位于 B+ 树中间的某个随机物理页中。
- 页分裂机制:InnoDB 为了腾出空间塞入记录,不得不将这个已满的页一分为二(分裂成两个各占 50% 数据的页),这伴随着大量的物理行数据在磁盘内存间的搬运拷贝,以及 B+ 树上级索引节点结构的重构更新。
- 后果:页分裂产生大量的物理空间空洞(产生严重的页面物理碎片,空间利用率跌破 50%),且频繁的页分裂会导致写操作耗时呈指数级上升,写入吞吐瞬间雪崩。
三、 Redo/Undo/Binlog 三大日志与崩溃恢复 (Why-What-How-Deep)
- Why(为什么要有二阶段提交机制?)
- 痛点:MySQL 有两套日志——InnoDB 引擎专属的
redo log(保证事务持久性)与 Server 层的binlog(保证主从复制与归档)。如果两者写盘顺序是孤立的,在写完 redo log 后服务器突然崩溃停电,binlog 没能写入,就会导致主库重启后根据 redo log 恢复了事务,但从库因为没能收到对应的 binlog 导致主从数据发生毁灭性不一致。 - 解决:引入二阶段提交机制。
- 痛点:MySQL 有两套日志——InnoDB 引擎专属的
- What/How(二阶段提交核心交互流程)
- 第一阶段(Prepare 阶段):InnoDB 将事务操作修改写入内存中的 Redo Buffer,并刷盘写入磁盘上的 redo log,同时将事务状态标记为
prepare。 - 第二阶段(Commit 阶段):Server 层将事务执行 SQL 写入 binlog 并落盘成功。随后,通知 InnoDB 将 redo log 中的事务状态修改为
commit,事务宣告彻底提交成功。
- 第一阶段(Prepare 阶段):InnoDB 将事务操作修改写入内存中的 Redo Buffer,并刷盘写入磁盘上的 redo log,同时将事务状态标记为
- Deep(深入内核:二阶段崩溃判定逻辑与组提交 Group Commit 合并 fsync 优化)
- 崩溃恢复的精密判定算法:
- 当 MySQL 重启进行 Crash Recovery 时,会在线扫描 redo log,发现状态为
prepare的事务,会拿着该事务的 XID 去binlog文件中寻找相对应的记录:- 情况 A:binlog 中没有该记录:这说明崩溃发生在一阶段 prepare 之后、二阶段 binlog 落盘之前。由于 binlog 没写,主从一致性被破坏,因此 JVM 强制对其执行回滚(Rollback)操作,抹去一切痕迹。
- 情况 B:binlog 中已经存在该 XID 记录:这说明崩溃发生在 binlog 已经落盘成功之后、但 redo 状态还没来得及改为 commit 之前。既然 binlog 已经落盘,从库最终会同步,因此 JVM 强制将该 prepare 事务提交(Commit),保证了主从数据的绝对对齐。
- 当 MySQL 重启进行 Crash Recovery 时,会在线扫描 redo log,发现状态为
- 组提交 (Group Commit) 优化(高吞吐深水区):
- 每次事务提交,都必须调用操作系统的
fsync()强行把日志缓冲刷入磁盘,磁盘 I/O 极其高昂。 - 为了防止高并发下多个线程的 fsync 导致磁盘卡死,InnoDB 引入了 组提交(Group Commit):它将并发事务的提交分为三个阶段(Flush、Sync、Commit)并由三个不同的队列接管。在 Sync 阶段,由队列中的 Leader 线程代表所有并发线程发起一次
fsync()系统调用,瞬间将这一组并发事务的日志批量刷写落盘。这一设计将磁盘 I/O 合并,写吞吐拉升数倍。
- 每次事务提交,都必须调用操作系统的
- 崩溃恢复的精密判定算法:
四、 InnoDB 行锁与临键锁 Next-Key Locks (Why-What-How-Deep)
- Why(为什么 RR 隔离级别可以降服幻读?)
- 痛点:传统的隔离级别在可重复读下无法防御“当前读(如
SELECT FOR UPDATE)”下的幻读问题,因为即使给已有行加了锁,并发事务依然可以在空隙中插入新行。 - 解决:InnoDB 引入 Next-Key Locks(临键锁),锁住记录的同时,死死锁住记录之间的间隙,杜绝并发插入。
- 痛点:传统的隔离级别在可重复读下无法防御“当前读(如
- What(Next-Key Lock 锁结构)
- 临键锁(Next-Key Lock):是**记录锁(Record Lock)与间隙锁(Gap Lock)**的结合体。它锁定记录本身以及该记录左侧的开区间间隙(左开右闭,如
(5, 10])。
- 临键锁(Next-Key Lock):是**记录锁(Record Lock)与间隙锁(Gap Lock)**的结合体。它锁定记录本身以及该记录左侧的开区间间隙(左开右闭,如
- How(面试最高频:InnoDB 临键锁加锁与退化规则大汇总)
- 记住金牌法则:InnoDB 默认加锁单位是 Next-Key Lock。但在特定查询下会退化为行锁或间隙锁以提升并发度。 | 查询场景 | 索引类型 | 记录是否存在 | 锁退化结果 | | :--- | :--- | :--- | :--- | | 等值查询 | 唯一索引 / 主键 | 存在 (如
id=5) | 退化为 记录锁 (Record Lock),仅锁这 1 行本身 | | 等值查询 | 唯一索引 / 主键 | 不存在 (如id=7) | 退化为 间隙锁 (Gap Lock),锁住(5, 10)| | 等值查询 | 普通非唯一索引 | 存在 (如age=20) | 加上(10, 20]的 Next-Key Lock,并在右侧加一对临近的(20, 30)间隙锁 | | 等值查询 | 普通非唯一索引 | 不存在 (如age=25) | 退化为 间隙锁 (Gap Lock),锁住(20, 30)| | 范围查询 | 唯一索引 / 主键 | — (如id>=5 AND id<10) | 加上(5, 10]的 Next-Key Lock,其中id=5退化为记录锁 |
- 记住金牌法则:InnoDB 默认加锁单位是 Next-Key Lock。但在特定查询下会退化为行锁或间隙锁以提升并发度。 | 查询场景 | 索引类型 | 记录是否存在 | 锁退化结果 | | :--- | :--- | :--- | :--- | | 等值查询 | 唯一索引 / 主键 | 存在 (如
- Deep(深入源码:当前读幻读消除与死锁检测机制)
- 为什么 Gap Lock 在 RR 下可以防止幻读?
- 当执行
SELECT * FROM t WHERE id = 7 FOR UPDATE时,若id=7不存在,InnoDB 锁定间隙(5, 10)。 - 此时并发事务尝试执行
INSERT INTO t (id) VALUES (7),存储引擎在插入前,必须申请该区间的 插入意向锁(Insert Intention Lock)。而插入意向锁与已持有的间隙锁(Gap Lock)是互斥的,导致 INSERT 被强制阻塞,从而物理上彻底剿灭了幻读的可能。
- 当执行
- 死锁检测源码机制 (Deadlock Detection):
- 行级锁的细粒度引发了死锁可能。InnoDB 内部实现了
wait-for graph(等待图):它会把当前所有的锁请求关系构建成一棵有向图。 - 当发生锁等待时,后台线程(由
innodb_deadlock_detect控制)会采用深度优先搜索(DFS)在线扫描该等待图是否构成了闭环。如果发现闭环(即死锁),会根据事务权重自动挑选出 回滚开销最小(即修改行数最少)的那个事务强制执行回滚,主动释放持有的锁,从而瞬时解开死锁,恢复系统运行。
- 行级锁的细粒度引发了死锁可能。InnoDB 内部实现了
- 为什么 Gap Lock 在 RR 下可以防止幻读?
五、 慢 SQL 优化与 EXPLAIN 执行计划 (Why-What-How-Deep)
- Why(为什么生产环境必须监控并调优慢 SQL?)
- 痛点:一条未走索引的慢 SQL 在执行全表扫描时,会占用高昂的磁盘 I/O 和 CPU,这会导致大量的并发查询线程在连接池中堆积挂起,迅速引发线程饥饿,最终拖垮整个数据库。
- 解决:开启慢日志,利用
EXPLAIN刨析执行计划并精准调优。
- What(EXPLAIN 两个最具区分度的核心关注指标)
type(访问类型,大厂底线为range):system>const(唯一索引) >eq_ref>ref>range(索引范围) >>index(全索引树扫描) >ALL(全表扫描)。
Extra(额外标志列,两大致命警告):Using filesort:文件排序。致命警告!说明无法利用索引本身进行排序,必须在内存或临时磁盘进行外部排序。Using temporary:使用临时表。致命警告!常发生于 group by 没走索引,极度耗内存。
- How(大厂生产级 Explain 调优实战)
- 简历实战引用:在我们的支付幂等表中,就是为了避开 Change Buffer 失效造成的磁盘 I/O 阻塞,而对 Buffer Pool 参数进行了调优。👉 点击跳转简历场景二
- Deep(深入内核:Filesort 的双路与单路排序算法剖析)
- 当 EXPLAIN 显示
Using filesort时,MySQL 究竟是怎么在底层进行排序的?它取决于系统参数max_length_for_sort_data:- 双路排序(Two-Pass,老旧):
- 机制:只把“排序列”和“主键ID”拷入排序缓冲区
sort_buffer进行排序。排完序后,由于没有其他属性字段,必须拿着主键 ID 回表再去磁盘读取需要的其他数据列。 - 缺点:会引发海量的随机磁盘 I/O 回表开销,目前已基本被弃用。
- 机制:只把“排序列”和“主键ID”拷入排序缓冲区
- 单路排序(Single-Pass,主流):
- 机制:如果 SQL 需要的所有字段的总大小小于
max_length_for_sort_data限制,MySQL 会将所有要查询的属性列一次性全部塞入sort_buffer,排序完毕后直接返回,免去了任何二次回表开销。 - 缺陷:如果需要排序的数据量过大,
sort_buffer被迅速填满溢出,MySQL 会被迫把数据切分到多个磁盘临时文件中进行归并排序(Merge-Sort),导致排序吞吐骤然滑坡。
- 机制:如果 SQL 需要的所有字段的总大小小于
- 👉 终极解法:为
(过滤字段, 排序字段)建立联合索引。因为 B+ 树本身天然就是按联合索引排序好的,MySQL 在检索出数据后,无需在内存中执行任何二次排序,直接返回,一举斩断Using filesort!
- 双路排序(Two-Pass,老旧):
- 当 EXPLAIN 显示
🎯 简历亮点深度关联与对线场景 (Why-What-How)
场景一:订单状态机条件更新与【MySQL 隐式锁与写写冲突】
面试官切入点:
“你在苍穹外卖中提到了通过‘条件更新(SQL 乐观锁)实现订单并发原子写’。请问,当并发线程同时执行
UPDATE orders SET status = 'CANCELLED' WHERE id = 123 AND status = 'PENDING'这条 SQL 时,在 MySQL 数据库底层发生了什么?它是如何保证不会把同一个订单取消两次的?”
回答思路 (Why-What-How-Deep 拆解):
- Why:在多端并发(例如用户在手机端取消,同时客服在后台强制取消)时,必须绝对防止订单状态机的二义性流转(如多次扣减或重复释放库存),保证并发写状态一致。
- What/How: 我们没有在 Java 层使用高昂的同步悲观锁(这会降低 QPS 吞吐),而是将乐观锁思想通过 SQL 条件更新融入数据库层:
UPDATE orders SET status = 'CANCELLED' WHERE id = 123 AND status = 'PENDING'。 - Deep:
- 行级排他锁 (X 锁) 强加锁:虽然 Java 层是乐观无锁的,但在 MySQL 执行该
UPDATE时,InnoDB 会立即自动为该行记录加 行级排他锁 (X 锁)。 - 锁排队与可见性:当并发线程同时到达时,事务 A 抢先获得该行 X 锁并执行修改;事务 B 被强制挂起,进入锁等待状态(wait-for graph 审计)。
- 条件原子拦截:当事务 A 提交并释放锁后,事务 B 被唤醒并立刻抢到该行 X 锁。但由于我们在 SQL 中设计了严格的
AND status = 'PENDING'过滤条件,此时该行记录在 MVCC 及物理页中已经由事务 A 变为了'CANCELLED'。因此,事务 B 的这行 SQL 无法匹配到任何数据,影响行数rows = 0。 - 我们在 Java 业务层通过检验
affectedRows == 0,主动抛出自定义乐观锁异常并给客户端报错,完美降服了重复流转的问题,保证了高并发下的账期安全!
- 行级排他锁 (X 锁) 强加锁:虽然 Java 层是乐观无锁的,但在 MySQL 执行该
场景二:支付回调幂等表与【B+ 树唯一索引秒杀级检索与 Change Buffer 避坑】
面试官切入点:
“在支付保障部分,你设计了‘支付回调记录表 + 唯一约束’方案来拦截重复回调,确保支付幂等性。为什么这能起到拦截作用?数据库的唯一索引底层是如何实现的?在高并发回调涌入时,会不会拖慢数据库性能?”
回答思路 (Why-What-How-Deep 拆解):
- Why:微信/支付宝等三方支付在网络波动时会发起高频重复回调,如果重复消费会导致多次发货或财务损失,必须在毫秒级实现强力去重幂等拦截。
- What/How: 我们设计了以“商户订单号
out_trade_no”作为唯一索引 (Unique Index) 的支付回调记录表。当高并发重复请求到达时,并发事务执行INSERT操作,后到达的事务由于唯一键冲突,底层直接抛出DuplicateKeyException(MySQL 错误码 1062),我们在 Java 层捕获该异常,判定为已处理,直接返回“SUCCESS”,安全拦截了重复支付逻辑。 - Deep:
- 唯一性检查开销与 Change Buffer 避坑:
- 普通非唯一索引在写入时拥有极高的吞吐,因为它能完美利用 Change Buffer(写缓冲区):如果对应的数据页当前不在 Buffer Pool 内存中,InnoDB 不会去随机读取磁盘,而是把写入动作临时写在 Change Buffer 里立即返回,极大地斩断了随机磁盘 I/O。
- 唯一索引彻底无缘 Change Buffer:因为唯一索引插入时必须保证唯一性。为了验证是否冲突,InnoDB 必须强行把对应的磁盘数据页读入 Buffer Pool 内存 中进行排重检测。在高并发写涌入时,这会产生高昂的随机磁盘 I/O。
- 大厂级优化实战:
- 预估到这一唯一性检查对 Change Buffer 的失效影响,我们通过提前对数据库分配充足的
innodb_buffer_pool_size(通常是服务器物理内存的 70%~80%),让绝大多数热点数据页常驻内存。 - 唯一性排重校验完全在内存中高速完成,避免了随机磁盘物理寻道,从而在唯一索引的铁桶拦截保障下,跑出了秒杀级的高吞吐写入性能!
- 预估到这一唯一性检查对 Change Buffer 的失效影响,我们通过提前对数据库分配充足的
- 唯一性检查开销与 Change Buffer 避坑:
📝 第三优先级:避坑与实战常识
- 主键自增无序化 UUID 对 B+ 树页分裂的底层影响
- 很多同学在建表时喜欢用
UUID作为聚簇索引的主键。但在高并发、海量写入表(如秒杀表、订单明细表)中,这是极其致命的灾难。 - 成因剖析:UUID 是完全随机、无序的。在插入数据时,新行主键可能非常小,也可能非常大。这会强制 InnoDB 为了将新记录按主键升序保存在对应的 B+ 树物理页中,不得不频繁将已经写满的 16KB 页从中劈开,发生页分裂(Page Split)。
- 物理惩罚:页分裂会导致磁盘产生大量物理空间碎片,空间利用率跌破 50%,且频繁的物理数据页拷贝会让磁盘 I/O 陷入死锁式等待,写入吞吐瞬间呈断崖式下跌。
- 大厂规范:高并发写入表必须强制使用具有单调递增属性的主键(如数据库自增 ID,或分布式架构下的雪花算法 ID、号段模式 ID),保证数据只往末尾页追加追加,彻底封杀页分裂!
- 很多同学在建表时喜欢用